#include "contraption.hh"

#include <string>
#include <vector>
#include <fstream>
#include <algorithm>
#include <iostream>

Contraption::Contraption(const std::string& filepath) :
    _data{}
{
    std::ifstream infile {filepath};
    std::string line {};
    while (std::getline(infile, line))
        this->_data.emplace_back(std::move(line));
}

std::size_t Contraption::routing(const Position& begin, LaserDirection direction) const
{
    std::map<Position, std::vector<LaserDirection>> passed {};
    this->doRouting(begin, direction, passed);
    return passed.size();
}

std::size_t Contraption::maxRouting() const
{
    std::size_t maxValue {0};
    std::size_t rowSize {this->_data.size()};
    std::size_t colSize {this->_data.front().size()};

    auto updateMax = [&maxValue] (std::size_t value) {
        if (maxValue < value) maxValue = value;
    };

    // First row
    for (std::size_t c = 0; c < colSize; ++c)
    {
        std::size_t value {this->routing({0, c}, LaserDirection::Down)};
        updateMax(value);
    }

    // Last row
    for (std::size_t c = 0; c < colSize; ++c)
    {
        std::size_t value {this->routing({rowSize - 1, c}, LaserDirection::Up)};
        updateMax(value);
    }

    // Left col
    for (std::size_t r = 0; r < rowSize; ++r)
    {
        std::size_t value {this->routing({r, 0}, LaserDirection::Right)};
        updateMax(value);
    }

    // Right col
    for (std::size_t r = 0; r < rowSize; ++r)
    {
        std::size_t value {this->routing({r, rowSize - 1}, LaserDirection::Left)};
        updateMax(value);
    }

    return maxValue;
}

void Contraption::doRouting(
        const Position& begin, 
        LaserDirection direction, 
        std::map<Position, std::vector<LaserDirection>>& passed) const
{
    Position pos {begin};

    // Keep direction util not '.'
    while (true)
    {
        // Save this pos 
        auto iter {passed.find(pos)};
        if (iter == passed.end())
            // Not found, pos never reached!
            passed.emplace(pos, std::vector<LaserDirection>{direction});
        else
        {
            // Found, add the direction in iter->second
            auto directionRes {std::find(iter->second.begin(), iter->second.end(), direction)};
            if (directionRes == iter->second.end())
                // Not this direction before, add it
                iter->second.emplace_back(direction);
            else
                // The same direction pass this pos, no need to continue
                break;
        }

        // Get Next Position
        char ch {this->character(pos)};
        switch (ch)
        {
        case '.':
        {
            // Keep direction
            switch (direction)
            {
            case LaserDirection::Left: 
            {
                pos.second -= 1;
                if (this->invalidCol(pos.second)) return;
                break;
            }
            case LaserDirection::Right:
            {
                pos.second += 1;
                if (this->invalidCol(pos.second)) return;
                break;
            }
            case LaserDirection::Up:
            {
                pos.first -= 1;
                if (this->invalidRow(pos.first)) return;
                break;
            }
            case LaserDirection::Down:
            {
                pos.first += 1;
                if (this->invalidRow(pos.first)) return;
                break;
            }
            }
            break;
        }
        case '/':
        {
            switch (direction)
            {
            case LaserDirection::Left:
            {
                direction = LaserDirection::Down;
                pos.first += 1;
                if (this->invalidRow(pos.first)) return;
                break;
            }
            case LaserDirection::Right:
            {
                direction = LaserDirection::Up;
                pos.first -= 1;
                if (this->invalidRow(pos.first)) return;
                break;
            }
            case LaserDirection::Up:
            {
                direction = LaserDirection::Right;
                pos.second += 1;
                if (this->invalidCol(pos.second)) return;
                break;
            }
            case LaserDirection::Down:
            {
                direction = LaserDirection::Left;
                pos.second -= 1;
                if (this->invalidCol(pos.second)) return;
                break;
            }
            }
            break;
        }
        case '\\':
        {
            switch (direction)
            {
            case LaserDirection::Left:
            {
                direction = LaserDirection::Up;
                pos.first -= 1;
                if (this->invalidRow(pos.first)) return;
                break;
            }
            case LaserDirection::Right:
            {
                direction = LaserDirection::Down;
                pos.first += 1;
                if (this->invalidRow(pos.first)) return;
                break;
            }
            case LaserDirection::Up:
            {
                direction = LaserDirection::Left;
                pos.second -= 1;
                if (this->invalidCol(pos.second)) return;
                break;
            }
            case LaserDirection::Down:
            {
                direction = LaserDirection::Right;
                pos.second += 1;
                if (this->invalidCol(pos.second)) return;
                break;
            }
            }
            break;
        }
        case '|':
        {
            switch (direction)
            {
            case LaserDirection::Left:
            case LaserDirection::Right:
            {
                // Recurive call
                // Go Up 
                Position pos1 {pos.first - 1, pos.second};
                if (!this->invalidRow(pos1.first)) 
                    this->doRouting(pos1, LaserDirection::Up, passed);
                
                // Go Down
                Position pos2 {pos.first + 1, pos.second};
                if (!this->invalidRow(pos2.first))
                    this->doRouting(pos2, LaserDirection::Down, passed);

                return;
            }
            case LaserDirection::Up:
            {
                pos.first -= 1;
                if (this->invalidRow(pos.first)) return;
                break;
            }
            case LaserDirection::Down:
            {
                pos.first += 1;
                if (this->invalidRow(pos.first)) return;
                break;
            }
            }
            break;
        }
        case '-':
        {
            switch (direction)
            {
            case LaserDirection::Up:
            case LaserDirection::Down:
            {
                // Recurive call
                // Go Left 
                Position pos1 {pos.first, pos.second - 1};
                if (!this->invalidCol(pos1.second)) 
                    this->doRouting(pos1, LaserDirection::Left, passed);
                
                // Go Right
                Position pos2 {pos.first, pos.second + 1};
                if (!this->invalidCol(pos2.second))
                    this->doRouting(pos2, LaserDirection::Right, passed);

                return;
            }
            case LaserDirection::Left: 
            {
                pos.second -= 1;
                if (this->invalidCol(pos.second)) return;
                break;
            }
            case LaserDirection::Right:
            {
                pos.second += 1;
                if (this->invalidCol(pos.second)) return;
                break;
            }
            }
            break;
        }
        default: exit(1);
        }
    }
}

char Contraption::character(const Position& pos) const
{
    return this->_data[pos.first][pos.second];
}

bool Contraption::invalid(const Position& pos) const
{
    return invalidRow(pos.first) || invalidCol(pos.second);
}

bool Contraption::invalidRow(std::int64_t row) const
{
    return row < 0 || row >= this->_data.size();
}

bool Contraption::invalidCol(std::int64_t col) const
{
    return col < 0 || col >= this->_data.front().size();
}